package com.uinnova.product.eam.web.eam.mvc;

import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import com.binary.core.exception.BinaryException;
import com.binary.core.lang.StringUtils;
import com.binary.core.util.BinaryUtils;
import com.binary.framework.web.RemoteResult;
import com.uinnova.product.eam.config.FilterUtil;
import com.uinnova.product.vmdb.comm.doc.annotation.ModDesc;
import com.uinnova.project.base.diagram.comm.constant.CommonConst;
import com.uinnova.project.base.diagram.comm.diagram.ESEnterpriseSysUserQuery;
import com.uinnova.project.base.diagram.comm.model.ESEnterpriseSysUser;
import com.uinnova.project.base.diagram.exception.LoginFailException;
import com.uinnova.project.base.diagram.util.RedisUtil;
import com.uinnova.project.service.eam.IEamSysSvc;
import com.uino.api.client.permission.IRoleApiSvc;
import com.uino.api.client.permission.IUserApiSvc;
import com.uino.bean.permission.base.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @Description 对接数字空间oauth2登录
 * @Author wang
 * @Date 2021-09-10-17:43
 * @version 1.0
 */
@Controller
@Slf4j
@RequestMapping("/wiki")
public class WikiController {

    @Value("${wiki.oauth.client.id}")
    private String CLIENT_ID;

    @Value("${wiki.oauth.client.secret}")
    private String CLIENT_SECRET;

    @Value("${wiki.oauth.client.user_agent}")
    private String USER_AGENT;

    @Value("${local.oauth.client.id}")
    private String LOCAL_CLIENT_ID;

    @Value("${local.oauth.client.secret}")
    private String LOCAL_CLIENT_SECRET;

    @Value("${local.oauth.client.user_agent}")
    private String LOCAL_USER_AGENT;

    @Value("${wiki.oauth.server.url}")
    private String oauthServerUrl;

    @Value("${wiki.oauth.server.token_callback.url}")
    private String callbackUrl;

    @Value("${wiki.oauth.client.grant_type}")
    private String grantType;

    @Value("${server.servlet.context-path:/}")
    private String contextPath;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private FilterUtil filterUtil;

    @Value("${monet.login.loginMethod}")
    private String loginMethod;

    @Autowired
    private IUserApiSvc userApiSvc;

//    @Value("${uino.user.rolename}")
//    private String roleName;

    @Value("${monet.use.topobuilder.thingjs}")
    private boolean thingJsUsed;

    @Autowired
    private IRoleApiSvc roleApiSvc;

    @Autowired
    private IEamSysSvc iEamSysSvc;

    private static final String TOKEN_STATUS_PREFIX = "STATUS_";

    private static final String TOKEN_VAL_PREFIX = "VAL_";

    @RequestMapping("/authRedirect")
    public ModelAndView authRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException, InterruptedException {
        //如果是getLoginStatus请求，直接返回重定向地址
        Object urlObj = request.getAttribute("url");
        ModelAndView modelAndView = new ModelAndView();
        if (urlObj != null) {
            String redirectUrl = (String) request.getAttribute("url");
            modelAndView.setView(new RedirectView(redirectUrl, true, false));
            return modelAndView;
        }
        //如果authorization等于空，直接返回401
        String authorization = request.getHeader(Header.AUTHORIZATION.getValue());
        if (StringUtils.isEmpty(authorization) || redisUtil.get(authorization) == null) {
            //返回401
            throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED,"登录凭证已过期，请重新登录");
        }
        /**
         *
         * 解决刷新token时线程并发的问题，
         * 实际场景为：5个请求带着过期token并发访问接口，如果不做控制，则会频繁的触发刷新token接口，并频繁的返回给前台401消息
         * 因为第一个请求刷新成功后，其他请求的刷新都会失败
         * 解决方法：
         *  对token字符串加锁，这样5个请求只有一个能去刷新token，其他的只能等待。
         *  拿到锁刷新token的请求在刷新成功后，在redis设置一个状态，以及老token和新token的键值对
         *  其他请求获得锁先判断是否有状态存在，有则根据老token获取新token的值，将新token设置到request请求头中，请求转发继续执行
         **/
        authorization = authorization.substring("Bearer ".length());
        String requestUri = (String) request.getAttribute("requestUri");
        synchronized (authorization.intern()) {
            Object authStatus = redisUtil.get(TOKEN_STATUS_PREFIX + authorization);
            if (authStatus == null) {
                RemoteResult remoteResult;
                try {
                    remoteResult = refreshToken(request);
                } catch (Exception e) {
                    log.info("刷新token失败");
                    throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED,"登录凭证已过期，请重新登录");
                }
                if (remoteResult == null) {
                    log.info("刷新token失败");
                    throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED,"登录凭证已过期，请重新登录");
                }
                String newToken = (String)remoteResult.getData();
                response.setHeader("serverstatus", "210");
                response.setHeader("tk", newToken);
                redisUtil.set("REFRESH" + authorization, newToken, TimeUnit.MINUTES.toSeconds(5L));
            } else {
                Object newTokenObj = redisUtil.get(TOKEN_VAL_PREFIX + authorization);
                if (newTokenObj == null) {
                    throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED,"登录凭证已过期，请重新登录");
                }
                String newToken =(String) newTokenObj;
                request.setAttribute(Header.AUTHORIZATION.getValue(), newToken);
            }
        }
        request.getRequestDispatcher(requestUri).forward(request, response);
        return modelAndView;
    }

    @RequestMapping("/getTokenByCode")
    public ModelAndView getTokenByCode(@RequestParam String code, @RequestParam String state, HttpServletRequest request, HttpServletResponse response) throws IOException {
        String referer = request.getHeader(Header.REFERER.getValue());
        boolean isLocal = false;
        if (!StringUtils.isEmpty(referer) && referer.contains("localhost")) {
            isLocal = true;
        }
        Object goPageUrlObj = redisUtil.get(state);
        if (BinaryUtils.isEmpty(goPageUrlObj)) {
            throw new LoginFailException("state前后不符")  ;
        }
        String goPageUrl = (String) goPageUrlObj;
        if (BinaryUtils.isEmpty(this.oauthServerUrl)) {
            log.info("认证地址不能为空");
            throw new BinaryException("认证地址不能为空");
        }

        //调用数字空间接口获取token
        Map<String, Object> params = new HashMap<>(16);
        params.put("grant_type", grantType);
        params.put("client_id", isLocal ? LOCAL_CLIENT_ID : CLIENT_ID);
        params.put("client_secret", isLocal ? LOCAL_CLIENT_SECRET : CLIENT_SECRET);
        params.put("code", code);
        params.put("redirect_uri", callbackUrl);
        HttpRequest httpRequest = HttpRequest.post(this.oauthServerUrl + "/oauth/token")
                .contentType(ContentType.FORM_URLENCODED.getValue())
                .header(Header.USER_AGENT, isLocal ? LOCAL_USER_AGENT : USER_AGENT)
                .form(params);
        HttpResponse tokenRes = httpRequest.execute();
        String responseBody = tokenRes.body();
        if (!tokenRes.isOk()) {
            log.info("获取token失败, 报错如下：" + responseBody);
            throw new LoginFailException("获取token失败, 报错：" + responseBody);
        }

        //调用数字空间接口获取用户信息
        JSONObject jsonObj = new JSONObject(responseBody);
        String originToken = jsonObj.getStr("access_token");
        String refreshToken = jsonObj.getStr("refresh_token");
        String returnToken = "tk=" + originToken;
        String accessToken = "Bearer " + originToken;
        HttpResponse userInfoRes = HttpRequest
                                        .get(this.oauthServerUrl + "/api/user/userinfo")
                                        .contentType(ContentType.JSON.getValue())
                                        .auth(accessToken)
                                        .header(Header.USER_AGENT, isLocal ? LOCAL_USER_AGENT : USER_AGENT)
                                        .execute();

        //判断请求是否成功
        if (!userInfoRes.isOk()) {
            log.info("获取用户信息失败, 报错如下：" + userInfoRes.body());
            throw new LoginFailException("获取用户信息失败");
        } else {
            SysUser currUser = filterUtil.getUserByWikiRes(userInfoRes.body());
            if (currUser == null) {
                throw new BinaryException("getTokenByCode-用户校验成功，获取详情失败");
            }
            redisUtil.set(accessToken, currUser, CommonConst.LOCAL_TOKEN_EXPIRE_TIME);
            redisUtil.set(originToken, refreshToken, CommonConst.LOCAL_TOKEN_EXPIRE_TIME);
            if (thingJsUsed) {
                // 根据用户id查询用户mmd_id
                Long currUserId = currUser.getId();
                ESEnterpriseSysUserQuery query = new ESEnterpriseSysUserQuery();
                query.setUserId(currUserId);
                SysUser sysUser = iEamSysSvc.getUserIdByMmdId(query);
                if (sysUser.getId() == null) {
                    // 调用数字空间接口获取用户信息
                    HttpResponse mmdIdUser = HttpRequest
                                                .get(this.oauthServerUrl + "/api/user/thingjs/get_mmd_id")
                                                .contentType(ContentType.JSON.getValue())
                                                .auth(accessToken)
                                                .header(Header.USER_AGENT, isLocal ? LOCAL_USER_AGENT : USER_AGENT)
                                                .execute();
                    if (!mmdIdUser.isOk()) {
                        log.info("获取当前用户MMD_ID失败, 报错如下：" + mmdIdUser.body());
                    } else {
                        JSONObject userJson = new JSONObject(mmdIdUser.body());
                        ESEnterpriseSysUser esEnterpriseSysUser = new ESEnterpriseSysUser();
                        esEnterpriseSysUser.setUserId(currUserId);
                        esEnterpriseSysUser.setMmdId(userJson.getLong("mmd_id"));
                        iEamSysSvc.saveOrUpdate(esEnterpriseSysUser);
                    }
                }
            }
        }
        if (goPageUrl == null || "".equals(goPageUrl.trim())
                || (!goPageUrl.trim().startsWith("http") && !goPageUrl.trim().startsWith("https"))) {
            try {
                goPageUrl = URLDecoder.decode(goPageUrl, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                Assert.isTrue(false, "url解码异常" + goPageUrl);
            }
            goPageUrl = callbackUrl.substring(0, callbackUrl.indexOf("/", 10)) + "/#/workbench";
            goPageUrl = goPageUrl + (!goPageUrl.contains("?") ? "?" : "&") + returnToken;
        } else {
            goPageUrl = goPageUrl + (!goPageUrl.contains("?") ? "?" : "&") + returnToken;
        }
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setView(new RedirectView(goPageUrl, true, false));
        return modelAndView;
    }

    @RequestMapping("/refreshToken")
    @ModDesc(desc = "刷新token", pDesc = "", rDesc = "true-已登录，false-未登录", rType = RemoteResult.class)
    public RemoteResult refreshToken(HttpServletRequest request) {
        String authorization = request.getHeader(Header.AUTHORIZATION.getValue());
        if (StringUtils.isEmpty(authorization)) {
            throw new LoginFailException("refreshToken-authorization不能为空");
        }
        String referer = request.getHeader(Header.REFERER.getValue());
        boolean isLocal = false;
        if (!StringUtils.isEmpty(referer) && referer.contains("localhost")) {
            isLocal = true;
        }
        String originToken = authorization.substring("Bearer ".length());
        Object refreshTokenObj = redisUtil.get(originToken);
        if (refreshTokenObj == null) {
            throw new LoginFailException("token无效，需要重新登录");
        } else {
            //调用数字空间接口刷新token
            String refreshToken = (String) refreshTokenObj;
            Map<String, Object> params = new HashMap<>(16);
            params.put("grant_type", "refresh_token");
            params.put("client_id", isLocal ? LOCAL_CLIENT_ID : CLIENT_ID);
            params.put("client_secret", isLocal ? LOCAL_CLIENT_SECRET : CLIENT_SECRET);
            params.put("refresh_token", refreshToken);
            HttpRequest httpRequest = HttpRequest.post(this.oauthServerUrl + "/oauth/refresh")
                                                 .contentType(ContentType.FORM_URLENCODED.getValue())
                                                 .header(Header.USER_AGENT, isLocal ? LOCAL_USER_AGENT : USER_AGENT)
                                                 .form(params);
            HttpResponse tokenRes = httpRequest.execute();
            if (!tokenRes.isOk()) {
                log.info("refreshToken-请求失败：{}", tokenRes.body());
                throw new LoginFailException("refreshToken-刷新token失败" + tokenRes.body());
            }
            String tokenResBody = tokenRes.body();
            JSONObject jsonObj = new JSONObject(tokenResBody);
            String newOriginToken = jsonObj.getStr("access_token");
            String newRefreshToken = jsonObj.getStr("refresh_token");
            String newAccessToken = "Bearer " + newOriginToken;

            //获取用户信息
            HttpResponse userInfoRes = HttpRequest.get(this.oauthServerUrl + "/api/user/userinfo")
                                                  .contentType(ContentType.JSON.getValue())
                                                  .auth(newAccessToken)
                                                  .header(Header.USER_AGENT, isLocal ? LOCAL_USER_AGENT : USER_AGENT)
                                                  .execute();

            //对返回信息进行处理
            if (!userInfoRes.isOk()) {
                log.info("refreshToken-获取用户信息失败");
                throw new LoginFailException("refreshToken-获取用户信息失败");
            } else {
                SysUser currUser = filterUtil.getUserByWikiRes(userInfoRes.body());
                if (currUser == null) {
                    log.info("刷新token-用户校验成功，获取详情失败");
                    throw new LoginFailException("用户校验成功，获取详情失败");
                }
                redisUtil.set(newAccessToken, currUser, CommonConst.LOCAL_TOKEN_EXPIRE_TIME);
                redisUtil.set(newOriginToken, newRefreshToken, CommonConst.LOCAL_TOKEN_EXPIRE_TIME);
                redisUtil.set(TOKEN_STATUS_PREFIX + originToken, 1, TimeUnit.HOURS.toSeconds(1L));
                redisUtil.set(TOKEN_VAL_PREFIX + originToken, newAccessToken, TimeUnit.MINUTES.toSeconds(1L));
            }
            return new RemoteResult(newOriginToken);
        }
    }
}
